/*
 * Copyright 2009 XueSong Guo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.webwheel.di.engine;

import cn.webwheel.di.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;

@SuppressWarnings("unchecked")
class ReflectTypeNativeProvider<T> implements TypeNativeProvider<T> {

    final DefaultContainer container;

    Map<String,Class<? extends Annotation>> allNamedDesire;

    final Key<T> key;

    MemberHelper<Constructor<T>> constructor;
    MemberHelper<Field>[] fields;
    MemberHelper<Method>[] methods;

    public ReflectTypeNativeProvider(DefaultContainer container, Key<T> key) {

        this.container = container;
        allNamedDesire = container.getAllNamedDesire();

        this.key = key;

        int m = key.toClass().getModifiers();
        if (Modifier.isInterface(m) || Modifier.isAbstract(m)) {
            throw new DIException("there's no bind class", key);
        }

        {
            Constructor<T>[] ctors = (Constructor<T>[]) key.toClass().getConstructors();
            if (ctors.length == 1) {
                constructor = new MemberHelper<Constructor<T>>();
                constructor.member = ctors[0];
            } else {
                for (Constructor<T> ctor : ctors) {
                    if(!accept(ctor)) continue;
                    if (constructor != null) {
                        throw new DIException("only one constructor can be marked inject to serve \"" + key.getName() + "\"", key);
                    }
                    constructor = new MemberHelper<Constructor<T>>();
                    constructor.member = ctor;
                }
                if (constructor == null) {
                    throw new DIException("there must be one construtctor marked inject to serve \"" + key.getName() + "\"", key);
                }
            }
            Desire an = getDesire(constructor.member);
            Type[] gpts = constructor.member.getGenericParameterTypes();
            Annotation[][] pass = constructor.member.getParameterAnnotations();
            constructor.ctxs = new Context[gpts.length];
            for (int i = 0; i < gpts.length; i++) {
                Desire pan = find(pass[i]);
                if(pan==null) pan = an;
                constructor.ctxs[i] = createContext(gpts[i], pan);
            }
        }
        {
            List<MemberHelper<Field>> list = new ArrayList<MemberHelper<Field>>();
            for (Field fd : key.toClass().getFields()) {
                if(!accept(fd)) continue;
                MemberHelper<Field> helper = new MemberHelper<Field>();
                helper.member = fd;
                helper.ctxs = new Context[1];
                helper.ctxs[0] = createContext(fd.getGenericType(), getDesire(fd));
                list.add(helper);
            }
            fields = list.toArray(new MemberHelper[list.size()]);
        }
        {
            List<MemberHelper<Method>> list = new ArrayList<MemberHelper<Method>>();
            for (Method mtd : key.toClass().getMethods()) {
                if(!accept(mtd)) continue;
                MemberHelper<Method> helper = new MemberHelper<Method>();
                helper.member = mtd;
                Type[] gpts = mtd.getGenericParameterTypes();
                Annotation[][] pass = mtd.getParameterAnnotations();
                helper.ctxs = new Context[gpts.length];
                Desire an = getDesire(mtd);
                for (int i = 0; i < gpts.length; i++) {
                    Desire pan = find(pass[i]);
                    if(pan==null) pan = an;
                    helper.ctxs[i] = createContext(gpts[i], pan);
                }
                list.add(helper);
            }
            Collections.sort(list, new Comparator<MemberHelper<Method>>() {
                public int compare(MemberHelper<Method> o1, MemberHelper<Method> o2) {
                    return o2.ctxs.length - o1.ctxs.length;
                }
            });
            methods = list.toArray(new MemberHelper[list.size()]);
        }
        // check for non public desire
        for (Constructor<?> ctor : key.toClass().getDeclaredConstructors()) {
            if(Modifier.isPublic(ctor.getModifiers())) continue;
            if (getDesire(ctor) != null) {
                throw new DIException("non public desire found at " + ctor, key);
            }
        }
        for (Field field : key.toClass().getDeclaredFields()) {
            if(Modifier.isPublic(field.getModifiers())) continue;
            if (getDesire(field) != null) {
                throw new DIException("non public desire found at " + field, key);
            }
        }
        for (Method method : key.toClass().getDeclaredMethods()) {
            if(Modifier.isPublic(method.getModifiers())) continue;
            if (getDesire(method) != null) {
                throw new DIException("non public desire found at " + method, key);
            }
        }
    }

    private Context createContext(Type type, Desire an) {
        String name = Desire.DefaultName, data = Desire.DefaultData;
        if (an != null) {
            name = an.value();
            data = an.data();
        }
        Context ctx = new Context();
        if (type instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) type;
            if (pt.getRawType() == Fetcher.class) {
                ctx.isFetcher = true;
                type = pt.getActualTypeArguments()[0];
            }
        }
        ctx.key = new Key(type, name);
        ctx.data = data;
        return ctx;
    }

    private Desire find(Annotation[] ans) {
        for (Annotation an : ans) {
            if(an instanceof Desire) return (Desire) an;
        }
        for (Annotation an : ans) {
            for (Map.Entry<String, Class<? extends Annotation>> entry : allNamedDesire.entrySet()) {
                if (entry.getValue().isInstance(an)) {
                    try {
                        return new NamedDesire(entry.getKey(), (String) an.getClass().getMethod("value").invoke(an));
                    } catch (Exception e) {
                        throw new DIException("impossible " + e.getMessage(), key);
                    }
                }
            }
        }
        return null;
    }

    private <T extends Annotation> T getAnnotation(Member member, Class<T> cls) {
        if (member instanceof Constructor) {
            return ((Constructor<T>)member).getAnnotation(cls);
        } else if (member instanceof Method) {
            return ((Method)member).getAnnotation(cls);
        } else {
            return ((Field)member).getAnnotation(cls);
        }
    }

    private Desire getDesire(Member member) {
        Desire desire = getAnnotation(member, Desire.class);
        if(desire!=null) return desire;
        for (Map.Entry<String, Class<? extends Annotation>> entry : allNamedDesire.entrySet()) {
            Annotation an = getAnnotation(member, entry.getValue());
            if (an != null) {
                try {
                    return new NamedDesire(entry.getKey(), (String) an.getClass().getMethod("value").invoke(an));
                } catch (Exception e) {
                    throw new DIException("impossible " + e.getMessage(), key);
                }
            }
        }
        return null;
    }

    private boolean accept(Member member) {
        Desire desire = getDesire(member);
        Serve serve = getAnnotation(member, Serve.class);
        if (serve == null && desire == null) return false;
        if (serve == null) return true;
        for (String s : serve.value()) {
            if(key.getName().matches(s)) return true;
        }
        return false;
    }

    private Object getInst(Context ctx) {
        if (ctx.isFetcher) {
            return container.createFetcher(ctx.key, ctx.data);
        }
        try {
            return container.getInstance(ctx.key, ctx.data);
        } catch (DIException e) {
            e.add(key);
            throw e;
        }
    }

    public T get() {
        Object[] args = new Object[constructor.ctxs.length];
        for (int i = 0; i < args.length; i++) {
            args[i] = getInst(constructor.ctxs[i]);
        }
        try {
            T obj = constructor.member.newInstance(args);
            for (MemberHelper<Field> field : fields) {
                field.member.set(obj, getInst(field.ctxs[0]));
            }
            for (MemberHelper<Method> method : methods) {
                args = new Object[method.ctxs.length];
                for (int i = 0; i < args.length; i++) {
                    args[i] = getInst(method.ctxs[i]);
                }
                method.member.invoke(obj, args);
            }
            return obj;
        } catch(DIException e) {
            e.add(key);
            throw e;
        } catch (Exception e) {
            throw new DIException("impossible " + e.getMessage(), key);
        }
    }

    static class Context {
        boolean isFetcher;
        Key key;
        String data;
    }

    static class MemberHelper<T extends Member> {
        T member;
        Context[] ctxs;
    }

    static class NamedDesire implements Desire {

        String name, data;

        NamedDesire(String name, String data) {
            this.name = name;
            this.data = data;
        }

        public String value() {
            return name;
        }

        public String data() {
            return data;
        }

        public Class<? extends Annotation> annotationType() {
            return Desire.class;
        }
    }
}
